Protocol Buffer文档翻译:Protocol Buffer基本用法:C++,Protocol Buffer Basics: C++
这个教程,用于向C++程序员介绍协议缓冲区(protocol buffers)的基本用法。通过建立一个简单示例程序的过程,此教程会向妳展示,如何做以下事情:
•. 在 .proto 文件中定义消息格式。
•.使用protocol buffer编译器。
•.使用C++的protocol buffer应用编程接口来写入及读取消息。
此教程并不是 一个 关于如何 在C++中使用protocol buffers的综合指南。 欲获取更详尽的参考信息,则阅读 Protocol Buffer语言指南 、 C++ 应用编程接口参考 、 C++生成 后的代码指南 和 编码参考 。
我们要使用的示例,是一个狠简单的"地址本"应用程序,它能够将联系人的信息写入到文件中,并且在日后读取回来。地址本中的每个人物,都拥有一个名字、编号、邮件地址和联系电话号码。
妳会如何对这样的结构化数据进行序列化及读取?对于这个问题,有多种解决方法:
•.内存中的原始数据结构可以以二进制形式来发送及保存。随着时间的推移,这种方式会变得越来越脆弱,因为,接收/读取方的代码必须以完全相同的内存布局、大小端设置等参数来编译。同时,随着以原始格式存储数据的文件以及按照该格式编写的软件的传播,要想对该格式进行扩展就越发地艰难了。
•. 妳可以自己发明一种格式,用来将数据内容编码成一个单个的字符串——例如,将4个整数编码为"12:3:-23:67"。这种方法,狠简单也狠灵活,不过,它需要编写一次性的编码及解码代码,并且,解码过程要耗费一些运行时资源。对于非常简单的数据,这种方式最好。
•. 将数据序列化为XML。这种方式,狠有吸引力,因为,XML是(某种程度上)人眼可读的,并且,狠多语言中都提供了对应的处理库。如果妳想与其它程序/项目共享数据的话,这是一个好方法。然而,XML有一个公认的毛病,那就是,太占用空间,并且,对它进行编码/解码也非常显著地影响到程序的性能。另外,对XML DOM 树进行遍历,也比对一般的类中的字段进行遍历要来得复杂。
Protocol buffers 就是专门为解决这种问题而开发的灵活、高效、自动的解决方案。 在使用 protocol buffers 的过程中, 妳编写一个 .proto 文件,用来描述妳想要存储的数据结构。根据那个文件, protocol buffer编译 器会生成一个类, 这个类会自动 以高效的二进制格式来对那个 protocol buffer 数据结构进行编码 及解码。 所生成的类,会提供针对 该protocol buffer 中 各个字段 的取值方法( getters ) 和 设值方法( setters ),并且 会处理好读取和写入过程中的各个细节,以便将该protocol buffer 作为一个单元来处理。 还有一个重要的特性,那就是, protocol buffer格式支持 妳在日后对该格式进行扩展,使得, 妳的代码仍然能够读取以旧格式编码的数据。
示例代码包含 于源代码压缩包中,具体位于"examples"目录 中。 到这里下载。
要开发妳的地址本程序,首先要创建一个 .proto 文件。 .proto 文件 中的定义是狠简单的 :对于 妳想要序列化的每种数据结构,都加入一个消息定义( message ),然后 ,为该消息中的每个字段指定一个名字和类型。 以下便是本教程中使用的 .proto 文件, addressbook.proto 。
package tutorial;
message Person {
required string name = 1;
required int32 id = 2;
optional string email = 3;
enum PhoneType {
MOBILE = 0;
HOME = 1;
WORK = 2;
}
message PhoneNumber {
required string number = 1;
optional PhoneType type = 2 [default = HOME];
}
repeated PhoneNumber phone = 4;
}
message AddressBook {
repeated Person person = 1;
}
妳应该感受到了,它的语法与C++或Java类似。让我们来详细研究一下这个文件中的各个部分,看看它们分别起到什么作用。
.proto 文件 的开头是一个包名声明, 它可以用来避免多个项目之间的命名冲突。 在 C++ 中,所生成的类会被放置到一个与包名相符的命名空间中。
接下来,就是消息定义了。消息,实际 上就是一个包含了一组各种类型的字段的聚合体。 有狠多标准的简单数据类型都可以作为字段类型来使用,包括: bool 、 int32 、 float 、 double 和 string 。 妳还可以将其它 的消息类型作为字段类型加入到妳的消息中—— 在上面的示例中, Person 消息 里包含着 PhoneNumber 消息, 而 AddressBook 消息 中又包含着 Person 消息 。 妳甚至还可以将消息类型嵌套地定义于其它消息内部——在本示例中, PhoneNumber 类型 即是定义于 Person 内部。 妳还可以定义枚举 ( enum ) 类型, 以确保某个字段 只取预定义列表中的某个值—— 本示例中, 我们指定了,电话号码 的类型 可以是 MOBILE 、 HOME 或 WORK 。
每个元素中" = 1"、" = 2"的标记,用来指定该字段在二进制编码中使用的唯一标识("tag")。1-15的标记值,要比其它的标记值少占用一个字节的编码空间,因此,作为一种优化手段,妳可以将这些标记值用于常用元素或重复元素,而将16及更大数字的标记值用于较少使用的可选元素。重复字段中的每个元素都需要将其标记值重新编码,因此,重复字段尤其适合于采用这种优化手段。
每个字段,必须使用以下某个修饰符来注解:
•. required : 此字段必须提供,否则,该消息会被认为是“未初始化的”("uninitialized")。如果 libprotobuf 是以调试(debug)模式编译的,则, 对一个未初始化的消息进行序列化,会引起一 个断言失败。 在 以 优化(optimized)模式编译的库中, 这个检查会被跳过, 该条消息会照常被写入。但是, 对一条未初始化的消息进行解析,是一定会失败(具体 就是,在解析方法中返回假( false ) )的。 除此之外,必选(required)字段与可选(optional)字段的行为完全相同。
•. optional :此字段可以存在,也可以不存在。如果 某个可选(optional)字段的值未被设置, 则, 会使用默认值。对于简单 的类型,妳可以指定自己的默认值, 在本示例中,我们就对电话号码(phone number)的 type 字段指定了默认值。如果 妳未指定默认值,则会使用系统默认值:数字类型 的默认值是0;字符串类型的默认值是空字符串;逻辑类型的默认值是假。对于嵌套 的消息,其默认值一定是 其消息的“默认实例”("default instance")或“原型”("prototype"), 即,任何字段都未设置。调用取值函数 去获取一个未显式设置值的可选(或必选)字段的值,会取到该字段的默认值。
•. repeated : 此字段可以重复任意次数 (包括 0次 ) 。那些 被重复的字段值,会在protocol buffer 中保留原有的顺序。 可以将重复字段当作动态改变尺寸的数组来看待。
必选(Required)属性是永久生效的。妳应当谨慎地将字段标记为必选( required )。如果,有朝一日,妳想要停止写入或发送某个必选字段的话,那么,在尝试将该字段变成可选字段的过程中就会遇到问题——旧的代码会将那些不包含该字段的消息当成是不完整的,因而会拒绝或丢弃对应的消息。妳应当考虑写一些与当前应用程序相关的自定义验证代码来验证消息的完整性,而不是依赖必选字段。 Google某些的工程师认为,使用 required ,其带来的害处比好处要大;它们倾向于只使用 optional 和 repeated 。不过,这只是部分人的看法。
在 Protocol Buffer语言指南 ,可以找到一个关于如何编写 .proto 文件的完整指南,包括所有可用的字段类型列表。 不要尝试去寻找类似于类继承的功能—— protocol buffers 不支持这种特性。
现在 ,已经写好了 .proto 。那么,下一个要做的事就是,生成对应 的类, 以便用来读取及写入 AddressBook (当然 还包括 Person 和 PhoneNumber )消息 。 为了完成这件事,妳需要针对妳的 .proto 文件来运行 protocol buffer 的编译器 protoc :
2. 现在 ,运行该编译器,指定以下参数: 源代码目录 ( 妳的应用程序的源代码所在的目录——如果妳未指定的话则会使用当前目录 ) ;目标目录 ( 生成的代码要放置到该目录中;通常与 $SRC_DIR 一致 ) ; 妳的 .proto 文件的路径。 在本示例中,即是:
protoc -I=$SRC_DIR --cpp_out=$DST_DIR $SRC_DIR/addressbook.proto
因为 妳想要的是C++类,所以指定 --cpp_out 选项——对于其它 被支持的语言,也提供了类似的选项。
这会在妳指定的目标目录中生成以下文件:
•. addressbook.pb.h ,头文件,其中声明了所生成的这些类。
•. addressbook.pb.cc ,其中包含着所生成的类的实现代码。
让我们来研究一下生成的代码,看看编译 器 为妳创建了哪些 类和方法。打开 addressbook.pb.h , 妳会发现, 其中 定义了若干个类,分别对应 着妳在 addressbook.proto 中定义的每种消息。具体 看 Person 这个类, 妳会发现,编译器为每个字段 都生成了访问函数。例如,针对 name 、 id 、 email 和 phone 字段 ,生成了这些方法:
// name
inline bool has_name() const ;
inline void clear_name();
inline const ::std:: string & name() const ;
inline void set_name( const ::std:: string & value);
inline void set_name( const char * value);
inline ::std:: string * mutable_name();
// id
inline bool has_id() const ;
inline void clear_id();
inline int32_t id() const ;
inline void set_id(int32_t value);
inline bool has_email() const ;
inline void clear_email();
inline const ::std:: string & email() const ;
inline void set_email( const ::std:: string & value);
inline void set_email( const char * value);
inline ::std:: string * mutable_email();
// phone
inline int phone_size() const ;
inline void clear_phone();
inline const ::google::protobuf:: RepeatedPtrField < ::tutorial:: Person_PhoneNumber >& phone() const ;
inline ::google::protobuf:: RepeatedPtrField < ::tutorial:: Person_PhoneNumber >* mutable_phone();
inline const ::tutorial:: Person_PhoneNumber & phone( int index) const ;
inline ::tutorial:: Person_PhoneNumber * mutable_phone( int index);
inline ::tutorial:: Person_PhoneNumber * add_phone();
妳可以看到,取值函数 (getters) ,其名字正是字段名字 的小写形式 ,而设值函数(setter)呢, 以 set_ 开头。 另外 ,对于每个单值 (可选 或必选 ) 字段,都有对应的 has 取值函数 ,如果该字段已被设置值,则该函数会返回真(true)。最后 ,每个字段都有一个 clear 方法,用于 将该字段恢复到空白状态。
数值 型的 id 字段, 只具有上面所说明的基本访问函数。 而 name 和 email 字段 却具有一些额外的方法,因为它们是字符串——一个 mutable_ 取值方法 ,用于获取 直接指向该字符串的一个指针,另外 还有一个额外的设值函数。注意 ,即使 email 尚未被设置值,妳一样可以调用 mutable_email() ; 它会被自 动初始化为一个空白字符串。如果 妳具有一个像这个例子中这样的单值消息字段,那么,它也会拥有一个 mutable_ 方法,但是不会有 set_ 方法。
重复字段 也拥有一些额外的特殊方法——观察 一下重复字段 phone 的方法, 妳会发现,能够做以下事情:
•. 检查 该重复字段的大小( _size ) ( 换句话说, 这个 Person 中关联了多少个电话号码 ) 。
•.使用下标来获取指定的电话号码。
•.更新指定下标处已有的电话号码。
•. 向消息中加入另一个电话号码,以便让妳随后对之进行编辑 (重复 的标量类型,拥有一个 add_ 方法,可用来直接传入新的值 ) 。
若想要详细了解编译 器针对特定的字段定义会生成哪些成员,则阅读 C++生成 的代码参考 。
生成 的代码中,包含一个名为 PhoneType 的枚举,它对应着妳在 .proto 文件中所写的枚举。 妳可以 以 Person::PhoneType 来引用这个类,并且,它的值 可以 是 Person::MOBILE 、 Person::HOME 和 Person::WORK (具体的实现细节比较复杂,但是,妳不需要理解个中细节,同样能够正常地使用枚举) 。
编译器 还为妳生成了一个嵌套类,名为 Person::PhoneNumber 。如果 妳查看具体代码的话,妳会发现, “真正的”类实际上名为 Person_PhoneNumber ,但是 Person 中定义的一个类型别名(typedef)使得妳能够将它当成嵌套类来使用。 唯一会造成差别的情况就是 当妳在另一个文件中对这个类进行前向声明的情况—— 妳无法在C++中对嵌套类进行前向声明,但是 妳可以对 Person_PhoneNumber 进行前向声明。
每个消息和构建器类,都还包含了一些其它方法,可用来检查或操作整个消息。这些方法包括:
•. bool IsInitialized() const; : 检查是否所有必选字段都已被设置。
•. string DebugString() const; : 返回一个人眼可读的字符串,用于描述该消息,对于调试尤其有用。
•. void CopyFrom(const Person& from); :使用指定消息中的值来覆盖本消息中的值。
•. void Clear(); : 将所有字段清除,恢复到空白状态。
这些方法,以及接下来要说明的输入/输出方法,实现了由所有 的C++ protocol buffer 类共享的 Message 接口。欲知更多信息, 则阅读 Message 的完整应用编程接口文档 。
最后 ,每个 protocol buffer 类,都提供了相应的方法,用来按照protocol buffer 二进制格式 对妳所选择的类型进行消息的写入和读取。 这些方法包括:
•. bool SerializeToString(string* output) const; : 将消息序列化, 并将得到的字节数组储存在指定的字符串中。注意 ,这个字节数组是二进制的,不是文本的; 我们只是将 string 类用作一个方便的容器。
•. bool ParseFromString(const string& data); :从指定的字符串中解析出一个消息。
•. bool SerializeToOstream(ostream* output) const; :将消息写入到指定的C++ ostream 。
•. bool ParseFromIstream(istream* input); :从指定的C++ istream 中解析出一个消息。
这些,仅仅是提供出来的所有解析及序列化方法中的一部分。同样地,欲查看完整列表,则阅读 Message 的应用编程接口参考 。
Protocol Buffers与面向对象设计 Protocol buffer类,本质上是一些简单的数据容器(类似于C++中的结构体);它们并不是面向对象模型中的一等公民。如果妳想要向生成的类中加入更丰富的行为,那么,最好的方式是使用某个类来将生成的protocol buffer类封装起来。同时,如果妳并无权控制该 .proto 文件的设计(例如,妳在复用另一个项目中的文件),那么,将protocol buffers 封装起来也是一个好主意。在那种情况下,妳可以利用该封装类来形成一个更适合该应用程序的独特环境的接口:隐藏某些数据和方法,暴露出某些便利函数,等等。妳不应该通过继承那些生成类的方式来向它们添加行为。这会破坏某些内部机制,同时也并不是一个好的面向对象开发方式。
好了,让我们来试着使用一下妳的protocol buffer 类。妳想要利用地址本程序来做的第一件事就是,向地址本文件中写入人物的详细信息。要实现这一点,妳需要创建那些protocol buffer 类的实例,并且填充其中的数据,然后将它们写入到某个输出流中。
以下这个程序,从文件中读入一个 AddressBook ,根据用户 的输入向其中加入一个新的 Person ,然后 将新的 AddressBook 重新写入到文件中。那些直接调用 或引用由编译 器 生成的代码的部分,已经高亮显示。
#include <iostream>
#include <fstream>
#include <string>
#include "addressbook.pb.h"
using namespace std;
// 这个函数,根据用户的输入来填充一个Person 消息。
void PromptForAddress(tutorial::Person* person) {
cout << "Enter person ID number: " ;
int id;
cin >> id;
person->set_id(id) ;
cin.ignore( 256 , '\n' );
cout << "Enter name: " ;
getline(cin, *person->mutable_name() );
cout << "Enter email address (blank for none): " ;
string email;
getline(cin, email);
if (! email.empty() ) {
person->set_email(email) ;
}
while ( true ) {
cout << "Enter a phone number (or leave blank to finish): " ;
string number;
getline(cin, number);
if (number.empty()) {
break ;
}
tutorial:: Person :: PhoneNumber * phone_number = person->add_phone();
phone_number->set_number(number);
cout << "Is this a mobile, home, or work phone? " ;
string type;
getline(cin, type);
if (type == "mobile" ) {
phone_number->set_type(tutorial:: Person ::MOBILE);
} else if (type == "home" ) {
phone_number->set_type(tutorial:: Person ::HOME);
} else if (type == "work" ) {
phone_number->set_type(tutorial:: Person ::WORK);
} else {
cout << "Unknown phone type. Using default." << endl;
}
}
}
// 主函数:从文件中读入整个地址本,
// 根据用户的输入来加入一个联系人,然后将地址本重新写入到同一个文件中。
int main(int argc, char* argv[]) {
// 验证 以确保 , 我们链接到的库,与我们编译时使用 的头文件的版本是兼容的。
GOOGLE_PROTOBUF_VERIFY_VERSION;
if (argc != 2 ) {
cerr << "Usage: " << argv[ 0 ] << " ADDRESS_BOOK_FILE" << endl;
return - 1 ;
}
tutorial:: AddressBook address_book ;
{
// 读入已有的地址本。
fstream input(argv[ 1 ], ios:: in | ios::binary);
if (!input) {
cout << argv[ 1 ] << ": File not found. Creating a new file." << endl;
} else if (! address_book. ParseFromIstream (&input) ) {
cerr << "Failed to parse address book." << endl;
return - 1 ;
}
}
// 加入 一个地址。
PromptForAddress ( address_book.add_person() );
{
// 将新的地址本重新写入到磁盘中。
fstream output(argv[ 1 ], ios:: out | ios::trunc | ios::binary);
if (! address_book. SerializeToOstream (&output) ) {
cerr << "Failed to write address book." << endl;
return - 1 ;
}
}
// 可选 :删除由libprotobuf 分配的所有全局对象。
google::protobuf:: ShutdownProtobufLibrary () ;
return 0 ;
}
注意其中 的 GOOGLE_PROTOBUF_VERIFY_VERSION 宏。尽管 并不是必要的,但是, 在使用C++ Protocol Buffer 库之前,执行 这个 宏 ,是个好习惯。 它能够检查以确保, 妳没有偶然链接到一个与编译时的头文件版本不兼容的 库版本上去。如果检测 到版本不匹配的情况,则,程序会退出。注意 ,每个 .pb.cc 文件 都会在启动时自动执行这个宏。
另外 也注意一下程序末尾的 ShutdownProtobufLibrary() 。 这个函数所做的事,就是,删除所有由Protocol Buffer 库分配的全局对象。 对于大部分程序来说,这也不是必要的,因为,进程已经 快要退出了,操作系统会处理 好内存回收的事。但是,如果 妳 在使用一个要求每个对象 都被释放的内存泄漏检查器,或者 ,如果妳在写一个可能 会被单个进程多次载入 及卸载的库,那么 , 妳就需要要求Protocol Buffers 清理所有痕迹了。
显然,一个地址本,如果妳无法读取其中的内容,那么,它就一点卵用也没有!以下示例代码,从上面示例所创建的文件中读取信息,并且输出到终端。
#include <iostream>
#include <fstream>
#include <string>
#include "addressbook.pb.h"
using namespace std;
// 遍历AddressBook 中的所有联系人,并且输出它们的信息。
void ListPeople(const tutorial:: AddressBook & address_book) {
for ( int i = 0 ; i < address_book.person_size() ; i++) {
const tutorial:: Person & person = address_book.person(i) ;
cout << "Person ID: " << person.id() << endl;
cout << " Name: " << person.name() << endl;
if ( person.has_email() ) {
cout << " E-mail address: " << person.email() << endl;
}
for ( int j = 0 ; j < person.phone_size() ; j++) {
const tutorial:: Person :: PhoneNumber & phone_number = person.phone(j) ;
switch ( phone_number.type() ) {
case tutorial:: Person ::MOBILE :
cout << " Mobile phone #: " ;
break ;
case tutorial:: Person ::HOME :
cout << " Home phone #: " ;
break ;
case tutorial:: Person ::WORK :
cout << " Work phone #: " ;
break ;
}
cout << phone_number.number() << endl;
}
}
}
// 主函数:从文件中读取整个地址本,然后输出其中的所有信息。
int main(int argc, char* argv[]) {
// 验证 以确保 , 我们链接到的库,与我们编译时使用 的头文件的版本是兼容的。
GOOGLE_PROTOBUF_VERIFY_VERSION;
if (argc != 2 ) {
cerr << "Usage: " << argv[ 0 ] << " ADDRESS_BOOK_FILE" << endl;
return - 1 ;
}
tutorial:: AddressBook address_book ;
{
// 读取已有 的地址本。
fstream input(argv[ 1 ], ios:: in | ios::binary);
if (! address_book. ParseFromIstream (&input) ) {
cerr << "Failed to parse address book." << endl;
return - 1 ;
}
}
ListPeople (address_book);
// 可选 :删除由libprotobuf 分配的所有全局对象。
google::protobuf:: ShutdownProtobufLibrary () ;
return 0 ;
}
在发布了以妳的protocol buffer 为基础的代码之后,迟早妳会发现,妳想要“改善”该protocol buffer 的定义。如果妳希望新的数据格式向后兼容,同时旧的数据格式向前兼容——妳一定想要实现这种效果——的话,那么,妳需要遵守一些规则。在新版本的protocol buffer 中:
•. 妳 不可以 改变任何已有字段 的标记(tag)数字。
•. 妳 不可以 新加入或删除任何必选(required)字段。
•. 妳 可以 删除可选 (optional)或重复(repeated)字段。
•. 妳 可以 添加 新的可选(optional)或重复(repeated)字段,但是 , 妳必须使用新的标记( tag )数字 ( 也就是说,必须使用之前从未在该 protocol buffer 中使用的标记数字,甚至 连已经被删除的字段的标记值也不能使用 ) 。
(对于 这些规则,有 一些例外 ,不过它们极少被使用。 )
如果 妳严格遵守这些规则,那么,旧代码就能够快乐地读取 新格式的消息,并且忽视 掉任何新字段。对于 旧代码来说,那些 被删除的可选字段,会具有其默认值, 而被删除的重复字段则会是空数组。 新代码也能够透明地读取旧格式的消息。然而 , 请注意, 新添加的可选字段不会出现在旧的消息中,因此 , 妳或者要使用 has_ 来显式地检查它们是否已被设置,或者 就 在 .proto 文件中的标记(tag)数字之后使 用 [default = value] 来提供一个有意义的默认值。如果 妳没有为某个可选元素指定默认 值,则, 会使用它的类型所对应的默认值:对于字符串 ,其默认值是空字符串。对于逻辑 值,默认值是假(false)。对于数字类型 ,默认值是0。另外 也要注意,如果妳添加了一个重复字段,那么, 在新代码中,妳无法区分它究竟是被(新代码)留空了还是( 旧代码 )根本就没有设置 值 ,因为 ,它没有 has_ 标记。
C++ Protocol Buffers库经过了高度的优化。但是,适当的使用方式还能将性能进一步提升。以下是一些提示,遵守这些提示的话,就能够榨干这个库中的每一滴性能:
•. 在可能的情况下,复用消息对象。消息, 会尽量保留它们曾经分配的内存,以便复用,即 使是妳清空了其内容也是如此。因此 ,如果 妳要处理相同类型 或类似结构 的多条消息,那么 ,应当 在每次都复用同一个消息对象,以减轻内存分配器的负担。但是,对象 会随着时间而变得臃肿,尤其 是, 当妳的消息的“形状”变化较大,或者偶然构造出一个 远超出普通尺寸的消息的情况下,更是如此。 妳应当调用 SpaceUsed 方法来监视消息对象的尺寸,并在它们变得过大时删除它们。
•. 妳的系统的内存分配器 在性能方面 可能并不适合于 在多个线程中 分配 大量小的对象。 试下使用 Google 的 tcmalloc 。
Protocol buffers 的用法,并不仅仅限于简单的访问函数和序列化。记得 要阅读 C++应用编程接口参考 ,以探索一下妳能够拿它来做什么用。
消息 类提供的其中一个关键特性是,反射( reflection )。 妳可以对一个消息的各个字段进行遍历,操作它们的值,而无需针对任何特定 的消息类型来编写代码。反射 的一个非常有用的用处就是, 在protocol buffers格式与其它格式之间互相转换,例如XML 或JSON。反射 的一个更高端的用法是,寻找 相同类型的两个消息之间的不同之处,或者 ,开发 出某种“用于protocol buffers消息的正则式”, 以便利用特定的表达式来匹配特定的消息内容。 发挥妳的想象力吧,妳会发现,Protocol Buffers 可以用来解决那些妳之前都没有预期到的问题!
反射 是由 Message::Reflection 接口 提供的 。
美人即将摔倒
林志玲
未知美人
未知美人
未知美人
未知美人
Your opinionsHxLauncher: Launch Android applications by voice commands